package com.googlecode.torrent4j.bencoding;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public class BEDecoder
{

	private InputStream stream;

	public BEDecoder(InputStream stream)
	{
		this.stream = new BufferedInputStream(stream);
	}

	int peekIndicator() throws IOException
	{
		stream.mark(1);
		int ind = stream.read();
		stream.reset();
		return ind;
	}

	public BEValue decode() throws IOException
	{
		int indicator = peekIndicator();
		if (indicator == -1)
		{
			return null;
		}
		else if (indicator >= '0' && indicator <= '9')
		{
			return bedecodeBytes();
		}
		else if (indicator == 'i')
		{
			return bedecodeNumber();
		}
		else if (indicator == 'l')
		{
			return bedecodeList();
		}
		else if (indicator == 'd')
		{
			return bedecodeDictionary();
		}
		else
		{
			throw new InvalidBEEncodingException("Imposible to decode stream");
		}
	}

	BEValue bedecodeBytes() throws IOException
	{
		int length = 0;
		int c = stream.read();

		while (Character.isDigit(c))
		{
			int digit = Character.digit(c, 10);
			length = length * 10 + digit;
			c = stream.read();
		}

		if (c != ':')
			throw new InvalidBEEncodingException("Expecting ':' character");

		byte[] data = new byte[length];
		int bytesRead = stream.read(data, 0, length);

		if (bytesRead != length)
			throw new InvalidBEEncodingException("Wrong byte array length");

		return new BEValue(data);
	}

	BEValue bedecodeNumber() throws IOException
	{

		int c = stream.read();
		if (c != 'i')
			throw new InvalidBEEncodingException("Wrong character, expected i got " + c);

		long number = 0;
		int multiplier = 1;
		c = stream.read();
		if (c == '-')
		{
			multiplier = -1;
			c = stream.read();
		}

		while (Character.isDigit(c))
		{
			int digit = Character.digit(c, 10);
			number = number * 10 + digit;
			c = stream.read();
		}

		if (c != 'e')
			throw new InvalidBEEncodingException("Wrong character, expected e got " + c);

		return new BEValue(multiplier * number);
	}

	BEValue bedecodeList() throws IOException
	{
		int c = stream.read();
		if (c != 'l')
			throw new InvalidBEEncodingException("Wrong character, expected l got " + c);

		List<BEValue> list = new ArrayList<BEValue>();

		while ((c = this.peekIndicator()) != 'e')
		{
			BEValue value = this.decode();
			if (value == null)
				throw new InvalidBEEncodingException("Unexpected end of stream");

			list.add(value);
		}

		c = stream.read();
		if (c != 'e')
			throw new InvalidBEEncodingException("Wrong character, expected l got " + c);

		return new BEValue(list);
	}

	BEValue bedecodeDictionary() throws IOException
	{
		int c = stream.read();
		if (c != 'd')
			throw new InvalidBEEncodingException("Wrong character, expected d got " + c);

		Map<String, BEValue> map = new LinkedHashMap<String, BEValue>();

		while ((c = this.peekIndicator()) != 'e')
		{
			BEValue keyValue = this.decode();
			if (keyValue == null)
				throw new InvalidBEEncodingException("Unexpected end of stream");

			String key = keyValue.stringValue();

			BEValue value = this.decode();

			if (key == null)
				throw new InvalidBEEncodingException("Unexpected end of stream");

			map.put(key, value);
		}

		c = stream.read();
		if (c != 'e')
			throw new InvalidBEEncodingException("Wrong character, expected l got " + c);

		return new BEValue(map);
	}
}
